<!-- 一个圆形菜单的组件 -->
<script setup lang="ts" generic="Item">
import { computed, nextTick, onMounted, onUnmounted, PropType, ref } from 'vue'

function debounce<Args extends any[], Context>(
  fn: (this: Context, ...args: Args) => void,
  delay = 100,
): (this: Context, ...args: Args) => void {
  let timer: ReturnType<typeof setTimeout> | undefined

  return function debounced(this: Context, ...args: Args) {
    if (timer) {
      clearTimeout(timer)
    }

    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

const props = defineProps({
  items: {
    type: Array as PropType<Item[]>,
    required: true,
  },
  // 菜单的半径，单位 px
  radius: {
    type: Number,
    default: 1000,
  },
  // 首个菜单项的角度，单位 rad，默认为9点钟方向
  startAngle: {
    type: Number,
    default: -Math.PI,
  },
  // 是否逆时针
  anticlockwise: {
    type: Boolean,
    default: true,
  },
  current: {
    type: undefined as any as PropType<Item>,
  },
  offset: {
    type: undefined as unknown as PropType<number | [number, number]>,
    default: () => [0, 0],
  },
})

const emit = defineEmits<{
  (type: 'update:current', item: Item): void
}>()

const anglePerItem = computed(() => (2 * Math.PI) / props.items.length)

const dragAngle = ref(0)

// 角度变化的趋势
let dragAngleDirection: 1 | -1 | 0 = 0

// 根据当前元素计算当前角度
const angle = computed(() => {
  let itemAngle = 0
  const vCurrent = props.current
  if (!vCurrent) {
    itemAngle = 0
  } else {
    // @ts-ignore
    itemAngle = props.items.indexOf(vCurrent) * anglePerItem.value
  }

  return props.startAngle + itemAngle + dragAngle.value
})

const offset = computed<[number, number]>(() => {
  if (Array.isArray(props.offset)) {
    return props.offset
  }
  return [props.offset, props.offset]
})

const positions = computed(() => {
  const { radius, anticlockwise } = props
  return props.items.map((_, index) => {
    const _angle = index * anglePerItem.value * (anticlockwise ? -1 : 1)
    return {
      x: radius * Math.cos(_angle) + radius,
      y: radius * Math.sin(_angle) + radius,
    }
  })
})

const cicle = ref<HTMLElement>()

function userEventEnd() {
  // @ts-ignore
  const oldIndex = props.items.indexOf(props.current!)
  const deltaIndex = Math.round(dragAngle.value / anglePerItem.value)
  let index = (deltaIndex + oldIndex) % props.items.length

  if (index < 0) {
    index += props.items.length
  }

  // @ts-ignore
  dragAngle.value = (deltaIndex - index + oldIndex) * anglePerItem.value

  console.log(dragAngle.value)

  emit('update:current', props.items[index])
}

const handleMouseWheelEnd = debounce(userEventEnd, 100)

const handleMouseWheel = (event: WheelEvent) => {
  dragAngle.value += Math.sign(event.deltaY) * anglePerItem.value
  handleMouseWheelEnd()
}

const handleMove = ref<undefined | ((event: MouseEvent | TouchEvent) => void)>(
  undefined,
)

const handleUserEventEnd = () => {
  if (handleMove.value) {
    userEventEnd()
    nextTick(() => {
      handleMove.value = undefined
    })
  }
}

const handleDocumentMove = (event: MouseEvent | TouchEvent) => {
  handleMove.value?.(event)
}

onMounted(() => {
  document.addEventListener('mousemove', handleDocumentMove)
  document.addEventListener('mouseup', handleUserEventEnd)
})

onUnmounted(() => {
  document.removeEventListener('mousemove', handleDocumentMove)
  document.removeEventListener('mouseup', handleUserEventEnd)
})

function getClinetOffset(event: MouseEvent | TouchEvent) {
  if ('touches' in event) {
    return [event.touches[0].clientX, event.touches[0].clientY]
  }
  return [event.clientX, event.clientY]
}

function getAngle(event: MouseEvent | TouchEvent) {
  const clientOffset = getClinetOffset(event)
  const rect = cicle.value!.getBoundingClientRect()
  const x = clientOffset[0] - rect.left - rect.width / 2
  const y = clientOffset[1] - rect.top - rect.height / 2
  return Math.atan2(y, x)
}

const handleStartMove = ($event: MouseEvent | TouchEvent) => {
  const startAngle = getAngle($event)
  let lastAngle = startAngle
  handleMove.value = (event: MouseEvent | TouchEvent) => {
    const currentAngle = getAngle(event)
    dragAngleDirection = Math.sign(currentAngle - lastAngle) as 1 | -1 | 0

    lastAngle = currentAngle
    dragAngle.value = currentAngle - startAngle
  }
}
</script>
<template>
  <div
    class="circle-menu"
    :style="{
      width: `${props.radius * 2}px`,
      height: `${props.radius * 2}px`,
    }"
  >
    <div
      class="circle"
      ref="cicle"
      style="
        transform-origin: center;
        position: absolute;
        transform: rotate(var(--angle));
      "
      @mousedown="handleStartMove"
      @touchstart="handleStartMove"
      @wheel.prevent="handleMouseWheel"
      :style="{
        '--angle': `${angle}rad`,
        transition: handleMove ? 'none' : 'transform 0.3s',
        top: `${offset[0]}px`,
        left: `${offset[1]}px`,
        width: `${props.radius * 2}px`,
        height: `${props.radius * 2}px`,
      }"
    >
      <div
        style="
          position: absolute;
          transform: rotate(0);
          transform-origin: center;
          transform: rotate(calc(-1 * var(--angle)));
        "
        v-for="(item, index) in items"
        :key="index"
        :style="{
          transition: handleMove ? 'none' : 'transform 0.3s',
          top: `${positions[index].y}px`,
          left: `${positions[index].x}px`,
        }"
      >
        <slot :item="item"></slot>
      </div>
    </div>
  </div>
</template>
<style scoped>
.circle-menu {
  position: relative;
  overflow: hidden;
  user-select: none;
  .circle {
    position: absolute;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
  }
}
</style>
